package org.shell.loaders;

import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.shell.commands.CommandBuilder;
import org.shell.commands.CommandBuilderImpl;
import org.shell.commands.CommandInvoker;
import org.shell.commands.CommandsHolder;
import org.shell.commands.exceptions.CommandAlreadyLoadedException;
import org.shell.commands.exceptions.CommandException;
import org.shell.loaders.annotations.CommandMethod;
import org.shell.loaders.annotations.ExternalObject;
import org.shell.loaders.annotations.Resource;
import org.shell.loaders.exceptions.LoaderGeneralException;
import org.shell.utils.config.ConfigImpl;

public class JarLoader implements Loader {

    private static CommandBuilder builder = CommandBuilderImpl.getCommandBuilder();
    private static HashMap<String, Class> classes = new HashMap<String, Class>();
    private static URLClassLoader urlLoader;

    public void load(String path, Boolean override) throws LoaderGeneralException {
        String resourceName;
        try {
            URLClassLoader urlLoader = getURLClassLoader(new URL("file", null, path));
            JarInputStream jis = new JarInputStream(new FileInputStream(path));
            JarEntry entry = jis.getNextJarEntry();
            while (entry != null) {
                resourceName = entry.getName();
                if (resourceName.endsWith(".class")) {
                    resourceName = resourceName.substring(0, resourceName.length() - 6);
                    resourceName = resourceName.replace('/', '.');
                    try {
                        Class c = urlLoader.loadClass(resourceName);
                        classes.put(resourceName, c);
                        if (!c.isInterface()) {
                            checkForMethodAnnotations(c, override);
                            checkForFieldsAnnotations(c);
                        }
                    } catch (Throwable e) {
                        e.printStackTrace();
                        System.out.println("Class not loaded:");
                        System.out.println("\t " + e.getClass().getName() + ": " + e.getMessage());
                    }
                }
                entry = jis.getNextJarEntry();
            }
        } catch (Exception ex) {
            throw new LoaderGeneralException("Wrong URL: " + path + "(" + ex.getLocalizedMessage() + ")");
        }
    }

    private void checkForMethodAnnotations(Class classInstance, Boolean overwrite) {
        for (Method m : classInstance.getDeclaredMethods()) {
            CommandMethod command = m.getAnnotation(CommandMethod.class);
            if (command != null) {
                try {
                    CommandsHolder.addCommand(builder.buildCommand(command.name(), classInstance, m, command.description()), overwrite);
                } catch (CommandAlreadyLoadedException e) {
                    System.out.println(e.getMessage());
                } catch (CommandException e) {
                    System.out.println(e.getLocalizedMessage());
                }
            }
        }
    }

    private void checkForFieldsAnnotations(Class classInstance) throws InstantiationException, IllegalAccessException {
        for (Field f : classInstance.getDeclaredFields()) {
            Resource res = f.getAnnotation(Resource.class);
            if (res != null) {
                f.setAccessible(true);
                switch (res.type()) {
                    case CONFIG:
                        f.set(CommandInvoker.getInstance(classInstance, false), ConfigImpl.getInstance());
                        break;
                    case CLASS:
                        Class classInterface = getClass(f.getType().getCanonicalName());
                        ExternalObject ext = (ExternalObject) classInterface.getAnnotation(ExternalObject.class);
                        String className = ext.className();
                        Class externalClassInstance = getClass(className);
                        Object instance = CommandInvoker.getInstance(externalClassInstance, Boolean.FALSE);
                        f.set(CommandInvoker.getInstance(classInstance, false), instance);
                        break;
                }
            }
        }
    }

    private static URLClassLoader getURLClassLoader(URL jarURL) {
        if (urlLoader == null) {
            urlLoader = new URLClassLoader(new URL[]{jarURL});
        } else {
            urlLoader = URLClassLoader.newInstance(new URL[]{jarURL}, urlLoader);
        }
        return urlLoader;
    }

    private Class getClass(String className) {
        try {
            return urlLoader.loadClass(className);
        } catch (ClassNotFoundException ex) {
            Logger.getLogger(JarLoader.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }
}
